#include "Camera.h"

using namespace DirectX;

Camera::Camera()
    : mPosW(0.0f, 0.0f, 0.0f)
    , mRightW(1.0f, 0.0f, 0.0f)
    , mUpW(0.0f, 1.0f, 0.0f)
    , mLookW(0.0f, 0.0f, 1.0f)
    , mNearZ(0.0f)
    , mFarZ(0.0f)
    , mAspectRatio(0.0f)
    , mFovY(0.0f)
    , mNearPlaneY(0.0f)
    , mFarPlaneY(0.0f)
{

}

void 
Camera::setFrustrum(const float fovY, const float aspect, const float nearPlaneZ, const float farPlaneZ) {
    // Cache frustrum properties
    mFovY = fovY;
    mAspectRatio = aspect;
    mNearZ = nearPlaneZ;
    mFarZ = farPlaneZ;
    const float halfFovY = 0.5f * mFovY;
    mNearPlaneY = 2.0f * mNearZ * tanf(halfFovY);
    mFarPlaneY  = 2.0f * mFarZ * tanf(halfFovY);

    // Cache projection matrix
    XMMATRIX proj = XMMatrixPerspectiveFovLH(mFovY, mAspectRatio, mNearZ, mFarZ);
    XMStoreFloat4x4(&mProj, proj);
}

void 
Camera::updateViewMatrix() {
    XMVECTOR rightW = XMLoadFloat3(&mRightW);
    XMVECTOR upW = XMLoadFloat3(&mUpW);
    XMVECTOR lookW = XMLoadFloat3(&mLookW);
    const XMVECTOR posW = XMLoadFloat3(&mPosW);

    // Keep camera's axes orthogonal to each other and of unit length.
    lookW = XMVector3Normalize(lookW);
    XMStoreFloat3(&mLookW, lookW);

    upW = XMVector3Cross(lookW, rightW);
    upW = XMVector3Normalize(upW);
    XMStoreFloat3(&mUpW, upW);

    rightW = XMVector3Cross(upW, lookW); 
    XMStoreFloat3(&mRightW, rightW);

    // Fill in the view matrix entries.
    const XMVECTOR dotPosWRightW = XMVector3Dot(posW, rightW);
    const XMVECTOR dotPosWUpW = XMVector3Dot(posW, upW);
    const XMVECTOR dotPosWLookW = XMVector3Dot(posW, lookW);
    const float x = -1.0f * XMVectorGetX(dotPosWRightW);
    const float y = -1.0f * XMVectorGetX(dotPosWUpW);
    const float z = -1.0f * XMVectorGetX(dotPosWLookW);

    mView(0, 0) = mRightW.x; 
    mView(1, 0) = mRightW.y; 
    mView(2, 0) = mRightW.z; 
    mView(3, 0) = x;   

    mView(0, 1) = mUpW.x;
    mView(1, 1) = mUpW.y;
    mView(2, 1) = mUpW.z;
    mView(3, 1) = y;  

    mView(0, 2) = mLookW.x; 
    mView(1, 2) = mLookW.y; 
    mView(2, 2) = mLookW.z; 
    mView(3, 2) = z;   

    mView(0, 3) = 0.0f;
    mView(1, 3) = 0.0f;
    mView(2, 3) = 0.0f;
    mView(3, 3) = 1.0f;
}

XMMATRIX 
Camera::viewProjMatrix() const {
    const XMMATRIX view = XMLoadFloat4x4(&mView);
    const XMMATRIX proj = XMLoadFloat4x4(&mProj);

    return XMMatrixMultiply(view, proj);
}

XMMATRIX
Camera::viewMatrix() const {
	return XMLoadFloat4x4(&mView);
}

XMMATRIX
Camera::projMatrix() const {
	return XMLoadFloat4x4(&mProj);
}

void 
Camera::setCoordinateSystem(FXMVECTOR posW, FXMVECTOR targetW, FXMVECTOR upW) {
    XMStoreFloat3A(&mPosW, posW);
        
    XMVECTOR lookW = XMVectorSubtract(targetW, posW);
    lookW = XMVector3Normalize(lookW);
    XMStoreFloat3A(&mLookW, lookW);

    XMVECTOR rightW = XMVector3Cross(upW, lookW);
    rightW = XMVector3Normalize(rightW);
    XMStoreFloat3A(&mRightW, rightW);

    XMVECTOR newUpW = XMVector3Cross(lookW, rightW);
    XMStoreFloat3A(&mUpW, newUpW);
}

void 
Camera::strafe(const float distance) {
    const XMVECTOR distanceW = XMVectorReplicate(distance);
    const XMVECTOR rightW = XMLoadFloat3A(&mRightW);
    const XMVECTOR posW = XMLoadFloat3A(&mPosW);
    const XMVECTOR newPosW = XMVectorMultiplyAdd(distanceW, rightW, posW);
    XMStoreFloat3A(&mPosW, newPosW);
}

void 
Camera::walk(const float distance) {
    const XMVECTOR distanceW = XMVectorReplicate(distance);
    const XMVECTOR lookW = XMLoadFloat3A(&mLookW);
    const XMVECTOR posW = XMLoadFloat3A(&mPosW);
    const XMVECTOR newPosW = XMVectorMultiplyAdd(distanceW, lookW, posW);
    XMStoreFloat3A(&mPosW, newPosW);
}

void
Camera::pitch(const float angle) {
    // We should rotate up and look vectors about right vector.
    const XMVECTOR rightW = XMLoadFloat3A(&mRightW);
    const XMMATRIX rotationW = XMMatrixRotationAxis(rightW, angle);

    const XMVECTOR upW = XMLoadFloat3A(&mUpW);
    const XMVECTOR newUp = XMVector3TransformNormal(upW, rotationW);
    XMStoreFloat3A(&mUpW, newUp);

    const XMVECTOR lookW = XMLoadFloat3A(&mLookW);
    const XMVECTOR newLookW = XMVector3TransformNormal(lookW, rotationW);
    XMStoreFloat3A(&mLookW, newLookW);
}

void 
Camera::rotateAboutYAxis(const float angle) {
    // We should rotate the basis vectors (right, up and look) 
    // about the world y-axis.
    const XMMATRIX rotationW = XMMatrixRotationY(angle);

    const XMVECTOR rightW = XMLoadFloat3A(&mRightW);
    const XMVECTOR newRightW = XMVector3TransformNormal(rightW, rotationW);
    XMStoreFloat3A(&mRightW, newRightW);

    const XMVECTOR upW = XMLoadFloat3A(&mUpW);
    const XMVECTOR newUpW = XMVector3TransformNormal(upW, rotationW);
    XMStoreFloat3A(&mUpW, newUpW);

    const XMVECTOR lookW = XMLoadFloat3A(&mLookW);
    const XMVECTOR newLookW = XMVector3TransformNormal(lookW, rotationW);
    XMStoreFloat3A(&mLookW, newLookW);
}

void 
Camera::translateThroughYAxis(const float distance) {
    const XMVECTOR distanceW = XMVectorReplicate(distance);
    const XMVECTOR upW = XMLoadFloat3A(&mUpW);
    const XMVECTOR posW = XMLoadFloat3A(&mPosW);
    const XMVECTOR newPosW = XMVectorMultiplyAdd(distanceW, upW, posW);
    XMStoreFloat3A(&mPosW, newPosW);
}